iT邦幫忙

2022 iThome 鐵人賽

DAY 29
0

今天試寫Python的多型。


Python就不必管甚麼「形式型別」和「實際型別」了

  • 前兩篇以C#為例介紹執行時期(run-time)多型。其中一大重點是「形式型別」和「實際型別」的不同與其效果。產生這個效果的源頭,是C#, Java等是靜態型別語言(statically typed language)(註1),於編譯時期,編譯器(compiler)就確定每一個變數和函數的型別(形式型別),從而在執行時期可以規範物件能與不能使用哪些方法。
  • 而Python的型別系統採用鴨子型別(duck type),其實就是動態型別語言(dynamically typed language)。變數和函數的型別要在執行時期才知道,而且允許像孫行者那樣七十二變,時而整數(int)時而字串(str)毫沒問題。如此一來就不會有甚麼形式型別、實際型別之分了。

  • 再貼一次昨天的多型分類圖以便講解:
    https://ithelp.ithome.com.tw/upload/images/20221013/20148485qDYycZm5wH.png
  • 由於Python基本上是直譯式(interpreted)語言,筆者不確定「編譯時期」和「執行時期」的說法,用在Python身上是否恰當,所以乾脆用上圖「左邊」和「右邊」來形容。
  • 筆者本來說這章只談上圖右邊部分。不過在找資料時,發現很多介紹Python多型的文章都從左邊這塊說起。筆者只好從俗,左邊部分也略談一下。

這也算是多型嗎?

  • 左邊是「同名異式」(overloading)。又可細分為「方法同名異式」(method overloading)和「運算子同名異式」(operator overloading)兩大類。運算子方面在多型第一篇(Day 27)的註解就有提及,這裡不再贅述。

  • 至於方法的同名異式,筆者已一再陳述,Python並不支援。既不支援又有甚麼好講?原來有人拿len()等標準函數為例,說len()的參數可以是個list,也可以是str(正確講法是:len()的參數要是個sequence或collection),所以len()就是個poly-morphic函數。

  • 又例如下面的sum_2_or_3_ints()函數,可以接受兩個或者三個整數型別的參數,有人認為這也算是多型:

    def sum_2_or_3_ints(n1: int, n2: int, n3: int=0):  
        return n1 + n2 + n3  
    
    print(sum_2_or_3_ints(3, 4))      # 7
    print(sum_2_or_3_ints(2, 5, 8))   # 15
    
  • 好吧,多型本來就是多種型態之意,那麼有多種不同型態的多型也未嘗不可。只是筆者並不認同就是。

  • 筆者所持論點是:真正的多型是要依不同物件去執行不同方法。即使是overloading也是不同方法。但以上的標準函數len()和自訂函數sum_2_or_3_ints(),就僅此一家別無分號,說也是多型,筆者覺得似乎有點過度解釋。

Python多型的雛形:以不同類別物件作為函數參數

  • 筆者認為這或許勉強算是Python的多型起點。

    class EarthlyTree():   # 地球上的樹
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} is growing.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} is reproducing.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} is getting sick.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} is dying.')
    
    
    class VenusianTree():   # 金星樹
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} grows younger and younger.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} reproduces on a daily basis.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} always revovers from illness.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} raises itself from the dead.')
    
    
    class MartianTree():   # 火星樹 
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} stops growing.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} does not reproduce.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} is healthy.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} is immortal.')
    
    
    # 主程式
    def universe(tree: object):   # 參數可為任何物件?
          tree.grow()  
          tree.reproduce()  
          tree.get_sick()
          tree.die()
    
    tree_on_earth = EarthlyTree('cedar', 2_500)
    tree_on_venus = VenusianTree('varan', -3_000)
    tree_on_mars = MartianTree('mvule', 500_000_000)
    
    print()  
    universe(tree_on_earth)
    print()  
    universe(tree_on_venus)
    print()
    universe(tree_on_mars)
    print()
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221014/20148485Mgp44lJP7k.png

  • 上例以不同類別的物件傳入universe()函數,達成「類多型」效果。

  • 不過這個程式存有危機,萬一傳入的物件並無grow(), reproduce(), get_sick(), die()等方法,鐵定出問題。

  • 此外,本例並未用到繼承。別忘了:真正意義的多型一定源自繼承。繼承和覆寫(overriding)是多型的基礎和預備。沒有「繼承」,很難認定那是貨真價實的「多型」。不是孕育自蚌的珍珠是真珍珠嗎?

以繼承機制實作Python多型

  • 下面就讓我們以繼承來實作多型吧:

    
    from abc import ABC, abstractmethod
    
    class Tree(ABC):
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @abstractmethod
        def grow(self):        # 生長(樹的共同行為)
            ...
    
        @abstractmethod        # 繁殖(樹的共同行為)
        def reproduce(self):
            ...
    
        @abstractmethod        # 生病(樹的共同行為)
        def get_sick(self):
            ...
    
        @abstractmethod        # 死亡(樹的共同行為)
        def die(self):
            ...
    
    class EarthlyTree(Tree):   # 地球上的樹
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} is growing.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} is reproducing.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} is getting sick.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} is dying.')
    
    
    class VenusianTree(Tree):   # 金星樹
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} grows younger and younger.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} reproduces on a daily basis.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} always revovers from illness.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} raises itself from the dead.')
    
    
    class MartianTree(Tree):   # 火星樹 
        def __init__(self, breed: str, age: int):   # constructor
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            is_valid_breed = True   # 判斷條件略。
            if is_valid_breed:
                self.__breed = breed
            else:
                raise Exception('Invalid breed.')
    
        @property
        def age(self) -> int:
            return self.__age
    
        @age.setter
        def age(self, breed: str):
            is_valid_age = True   # 判斷條件略。
            if is_valid_age:
                self.__age = age
            else:
                raise Exception('Invalid age.')
    
        def grow(self):        # 生長。
            print(f'{__class__.__name__} {self.breed} stops growing.')
    
        def reproduce(self):   # 繁殖。
            print(f'{__class__.__name__} {self.breed} does not reproduce.')
    
        def get_sick(self):     # 得病。
            print(f'{__class__.__name__} {self.breed} is healthy.')
    
        def die(self):          # 死亡(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} is immortal.')
    
  • 隨機抽兩棵樹看看:

    import random as r
    
    tree_on_earth = EarthlyTree('cedar', 2_500)
    tree_on_venus = VenusianTree('varan', -3_000)
    tree_on_mars = MartianTree('mvule', 500_000_000)
    
    trees = [tree_on_earth, tree_on_venus, tree_on_mars]
    randomly_selected_trees = r.sample(trees, k=2)
    
    for tree in randomly_selected_trees:
        tree.grow()
        tree.reproduce()
        tree.get_sick()
        tree.die()
        print()
    
  • 隨機產生金星樹和火星樹:
    https://ithelp.ithome.com.tw/upload/images/20221014/2014848535dG2hBey5.png

  • 再試在程式執行時期,從鍵盤輸入樹種,看能否動態呼叫到正確的方法:

    tree_infos = {'Earth': {'breed': 'cedar', 'age': 2_500}, 
                  'Venus': {'breed': 'varan', 'age': -3_000}, 
                  'Mars': {'breed': 'mvule', 'age': 500_000_000}
    }
    tree_on_earth = EarthlyTree(tree_infos['Earth']['breed'], tree_infos['Earth']['age'])
    tree_on_venus = VenusianTree(tree_infos['Venus']['breed'], tree_infos['Venus']['age'])
    tree_on_mars = MartianTree(tree_infos['Mars']['breed'], tree_infos['Mars']['age'])
    
    trees = {tree_infos['Earth']['breed']: tree_on_earth, tree_infos['Venus']['breed']: tree_on_venus, tree_infos['Mars']['breed']: tree_on_mars}
    
    breed = input('Enter a tree breed: ').strip().lower()
    tree = trees.get(breed)
    if tree is not None:
        tree.grow()
        tree.reproduce()
        tree.get_sick()
        tree.die()
        print()
    else:
        print('Oh, this tree is not in our list.  Pls check the spelling.')
    
  • 結果成功配對到正確的方法,並無張冠李戴,錯把馮京當馬涼:
    https://ithelp.ithome.com.tw/upload/images/20221014/20148485PDCx8aCevU.png


  • 假如真有人耐心將本系列文章從頭一路看下來(猜想很少),可能有人已經看出:Nothing new! 上例用的全都是系列文章之前談過的技術:繼承、覆寫、抽象方法,沒有新東西。把這些湊合起來,就構成Python的多型(如果筆者沒有誤解的話)。
  • 有個小問題:上面的Python程式,並沒有辦法像之前介紹的C#程式宣告父類別的型別,但實際new出子類別物件,從而規範子類別只能有哪些方法的效果,是為不足之處。究竟是筆者功力不足,未研究透徹(很有可能),還是真的不行?這問題就讓筆者再努力衝刺一下,看明天有無答案。

多型總結

  • 多型大分為編譯時期和執行時期兩種(此說應不適用於Python)。本系列著重於執行時期的多型。
  • 事實上執行時期的多型,才是物件導向的精華
  • 多型的上游是繼承。
  • 繼承的功能之一是程式碼重用(reuse),而多型則可讓子類別有一致的介面,易於日後擴充及維護。

註1: 英語教學時間:靜態、動態型別語言,英文的正確寫法是statically typed languagedynamically typed language,或簡稱static languagedynamic language。但不能寫static type languagedynamic type language(因不合英文文法)。如果指涉的是「型別系統」本身,通常用gerund:static typingdynamic typing。抱歉,筆者英語老師出身,職業病。


上一篇
「形式型別」故意和「實際型別」不同,作用何在?
下一篇
終點是下一段學習的起點
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言